
package com.epam.android.training.level1.db.dao.impl;

import com.epam.android.training.level1.db.dao.RssDao;
import com.epam.android.training.level1.db.rss.Rss.Category;
import com.epam.android.training.level1.db.rss.Rss.Feed;
import com.epam.android.training.level1.db.rss.Rss.Item;
import com.epam.android.training.level1.db.rss.RssDbHelper;
import com.epam.android.training.level1.rss.FeedBo;
import com.epam.android.training.level1.rss.ItemBo;
import com.epam.android.training.level1.rss.LightFeedBo;
import com.epam.android.training.level1.rss.NewsFeed;
import com.epam.android.training.level1.rss.RssParserException;
import com.epam.android.training.level1.rss.parser.FeedParser;
import com.epam.android.training.level1.rss.parser.Rss20FeedParser;
import com.epam.android.training.level1.util.IOUtils;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.provider.BaseColumns;
import android.util.Log;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * @author Anton Novikau
 */
public class RssDaoImpl implements RssDao {
  private static final String TAG = "RssDao";

  private static final String[] FULL_FEED_PROJECTION = {
      Feed._ID, Feed.TITLE, Feed.FEED_URL, Feed.LINK, Feed.ICON
  };

  private static final String[] FULL_ITEM_PROJECTION = {
      Item._ID, Item.FEED_ID, Item.TITLE, Item.LINK, Item.DESC, Item.PUB_DATE, Item.IS_READ
  };

  private static final String[] CATEGORY_PROJECTION = {
      Category._ID, Category.NAME
  };

  private static final String[] ID_PROJECTION = {
    BaseColumns._ID
  };

  private static final String[] FEED_URL_PROJECTION = {
      Feed._ID, Feed.FEED_URL
  };

  private static final String[] FEED_TITLE_PROJECTION = {
      Feed._ID, Feed.TITLE
  };

  private static final String[] ITEM_TITLE_PROJECTION = {
      Item._ID, Item.TITLE
  };

  private static final String INSERT_ITEM_STMT = "INSERT INTO " + Item.TABLE_NAME + "("
      + Item.FEED_ID + "," + Item.TITLE + "," + Item.LINK + "," + Item.DESC + "," + Item.PUB_DATE
      + ") VALUES (?,?,?,?,?)";

  private static final String INSERT_CATEGORY_STMT = "INSERT INTO " + Category.TABLE_NAME + "("
      + Category.FEED_ID + "," + Category.NAME + ") VALUES(?,?)";

  private static final String LATEST_NEWS_TITLE = "SELECT DISTINCT " + Item.TITLE + " FROM "
      + Item.TABLE_NAME + " where " + Item.PUB_DATE + " = (SELECT MAX(" + Item.PUB_DATE + ") from "
      + Item.TABLE_NAME + ")";

  private final RssDbHelper dbHelper;

  public RssDaoImpl(Context context) {
    dbHelper = new RssDbHelper(context);
  }

  @Override
  public boolean syncFeeds() {
    boolean status = true;
    SQLiteDatabase db = dbHelper.getReadableDatabase();
    Cursor feedCursor = null;
    try {
      feedCursor = db.query(Feed.TABLE_NAME, FEED_URL_PROJECTION, null, null, null, null,
          Feed.DEFAULT_SORT_ORDER);
      while (feedCursor.move(1)) {
        try {
          FeedParser parser = new Rss20FeedParser(feedCursor.getString(feedCursor
              .getColumnIndex(Feed.FEED_URL)));
          FeedBo feed = parser.parse();
          feed.setId(feedCursor.getLong(feedCursor.getColumnIndex(Feed._ID)));

          boolean syncStatus = updateFeed(feed);

          if (!syncStatus) {
            status = false;
          }
        } catch (MalformedURLException e) {
          // should never occur because URL stored to database in well-fromed
          // format.
          Log.e(TAG, "Unable to parse URL", e);
          status = false;
        } catch (RssParserException e) {
          Log.e(TAG, "Unable to parse feed.", e);
          status = false;
        }
      }
    } finally {
      IOUtils.closeCursor(feedCursor);
      db.close();
    }

    return status;
  }

  @Override
  public long createFeed(URL feedUrl) {
    FeedParser parser = new Rss20FeedParser(feedUrl);
    try {
      FeedBo feed = parser.parse();
      return insertFeed(feed);
    } catch (RssParserException e) {
      Log.e(TAG, "Unable to parse specified feed.", e);
    }
    return -1;
  }

  @Override
  public NewsFeed findFeed(long feedId, boolean isLight) {
    return findFeedCommon(buildStrictSelection(Feed._ID), new String[] {
      String.valueOf(feedId)
    }, isLight);
  }

  @Override
  public NewsFeed findFeed(URL feedUrl, boolean isLight) {
    return findFeedCommon(buildStrictSelection(Feed.FEED_URL), new String[] {
      feedUrl.toString()
    }, isLight);
  }

  @Override
  public List<NewsFeed> findAllFeeds() {
    List<NewsFeed> feeds = new ArrayList<NewsFeed>();
    SQLiteDatabase db = dbHelper.getReadableDatabase();
    Cursor feedCursor = null;
    try {
      feedCursor = db.query(Feed.TABLE_NAME, FULL_FEED_PROJECTION, null, null, null, null,
          Feed.DEFAULT_SORT_ORDER);
      while (feedCursor.move(1)) {
        feeds.add(cursorToFeed(feedCursor, true));
      }
    } finally {
      IOUtils.closeCursor(feedCursor);
      db.close();
    }

    return feeds;
  }

  @Override
  public String getLatestNewsTitle() {
    SQLiteDatabase db = dbHelper.getReadableDatabase();
    Cursor itemCursor = null;
    String latestNewsTitle = "";
    try {
      itemCursor = db.rawQuery(LATEST_NEWS_TITLE, null);
      if (itemCursor.moveToFirst()) {
        latestNewsTitle = itemCursor.getString(itemCursor.getColumnIndex(Item.TITLE));
      }
    } finally {
      IOUtils.closeCursor(itemCursor);
      db.close();
    }
    return latestNewsTitle;
  }

  @Override
  public String getFeedTitle(long feedId) {
    SQLiteDatabase db = dbHelper.getReadableDatabase();
    String title = null;
    Cursor c = null;
    try {
      c = db.query(Feed.TABLE_NAME, FEED_TITLE_PROJECTION, buildStrictSelection(Feed._ID),
          new String[] {
            String.valueOf(feedId)
          }, null, null, Feed.DEFAULT_SORT_ORDER);
      if (c.moveToFirst()) {
        title = c.getString(c.getColumnIndex(Feed.TITLE));
      }
    } finally {
      IOUtils.closeCursor(c);
      db.close();
    }

    return title;
  }

  @Override
  public List<ItemBo> findFeedItems(long feedId) {
    List<ItemBo> items = new ArrayList<ItemBo>();
    SQLiteDatabase db = dbHelper.getReadableDatabase();
    Cursor itemCursor = null;
    try {
      itemCursor = db.query(Item.TABLE_NAME, FULL_ITEM_PROJECTION,
          buildStrictSelection(Item.FEED_ID), new String[] {
            String.valueOf(feedId)
          }, null, null, Item.DEFAULT_SORT_ORDER);

      while (itemCursor.move(1)) {
        ItemBo item = new ItemBo();
        item.setId(itemCursor.getLong(itemCursor.getColumnIndex(Item._ID)));
        item.setFeedId(itemCursor.getLong(itemCursor.getColumnIndex(Item.FEED_ID)));
        item.setLink(itemCursor.getString(itemCursor.getColumnIndex(Item.LINK)));
        item.setTitle(itemCursor.getString(itemCursor.getColumnIndex(Item.TITLE)));
        item.setDescription(itemCursor.getString(itemCursor.getColumnIndex(Item.DESC)));
        Date pubDate = new Date(itemCursor.getLong(itemCursor.getColumnIndex(Item.PUB_DATE)));
        item.setPubDate(pubDate);
        item.setRead(itemCursor.getInt(itemCursor.getColumnIndex(Item.IS_READ)));

        items.add(item);
      }
    } finally {
      IOUtils.closeCursor(itemCursor);
      db.close();
    }
    return items;
  }

  @Override
  public List<String> findCategories(long feedId) {
    List<String> categories = new ArrayList<String>();
    SQLiteDatabase db = dbHelper.getReadableDatabase();
    Cursor catCursor = null;
    try {
      catCursor = db.query(Category.TABLE_NAME, CATEGORY_PROJECTION,
          buildStrictSelection(Category.FEED_ID), new String[] {
            String.valueOf(feedId)
          }, null, null, Category.DEFAULT_SORT_ORDER);

      while (catCursor.move(1)) {
        categories.add(catCursor.getString(catCursor.getColumnIndex(Category.NAME)));
      }
    } finally {
      IOUtils.closeCursor(catCursor);
      db.close();
    }
    return categories;
  }

  @Override
  public boolean markAsRead(long itemId) {
    SQLiteDatabase db = dbHelper.getWritableDatabase();

    int updated;
    try {
      ContentValues readFlag = new ContentValues();
      readFlag.put(Item.IS_READ, true);
      updated = db.update(Item.TABLE_NAME, readFlag, buildStrictSelection(Item._ID), new String[] {
        String.valueOf(itemId)
      });
    } finally {
      db.close();
    }
    return updated == 1;
  }

  private long insertFeed(FeedBo feed) {
    long feedId = -1;
    if (feed == null) {
      Log.w(TAG, "Unable to create empty feed.");
      return feedId;
    }

    SQLiteDatabase db = dbHelper.getWritableDatabase();
    SQLiteStatement insertItemStmt = null;
    SQLiteStatement insertCatStmt = null;
    try {
      db.beginTransaction();

      feedId = db.insert(Feed.TABLE_NAME, Feed.TITLE, toContentValues(feed));
      if (feedId > 0) {
        feed.setId(feedId);

        // create feed items in the same transaction.
        insertItemStmt = db.compileStatement(INSERT_ITEM_STMT);
        List<ItemBo> items = feed.getItems();
        for (ItemBo rssItem : items) {
          bindValues(feedId, rssItem, insertItemStmt);
          long itemId = insertItemStmt.executeInsert();
          rssItem.setId(itemId);
        }

        // create feed categories in the same transaction.
        insertCatStmt = db.compileStatement(INSERT_CATEGORY_STMT);
        List<String> categories = feed.getCategories();
        for (String cat : categories) {
          bindValues(feedId, cat, insertCatStmt);
          insertCatStmt.executeInsert();
        }
        db.setTransactionSuccessful();
      } else {
        Log.e(TAG, "Feed is not created.");
      }
    } finally {
      if (db.inTransaction()) {
        db.endTransaction();
      }
      IOUtils.closeStmt(insertItemStmt);
      IOUtils.closeStmt(insertCatStmt);
      db.close();
    }
    return feedId;
  }

  private boolean updateFeed(FeedBo feed) {
    if (feed == null) {
      Log.w(TAG, "Unable to update empty feed.");
      return false;
    }

    SQLiteDatabase db = dbHelper.getWritableDatabase();
    SQLiteStatement insertItemStmt = null;
    SQLiteStatement insertCatStmt = null;
    boolean isUpdated = false;
    try {
      db.beginTransaction();
      ContentValues feedData = toContentValues(feed);
      Long feedId = feed.getId();

      int updated = db.update(Feed.TABLE_NAME, feedData, buildStrictSelection(Feed._ID),
          new String[] {
            String.valueOf(feedId)
          });

      isUpdated = updated == 1;

      List<ItemBo> items = feed.getItems();

      // insert only new items to the feed.
      insertItemStmt = db.compileStatement(INSERT_ITEM_STMT);
      for (ItemBo item : items) {
        if (!isItemExists(feedId, item.getLink(), db)) {
          // add new items to the feed. RSS items must not be updated.
          bindValues(feedId, item, insertItemStmt);
          insertItemStmt.executeInsert();
        }
      }

      // clear old categories.
      db.delete(Category.TABLE_NAME, buildStrictSelection(Category.FEED_ID), new String[] {
        String.valueOf(feedId)
      });

      // create feed categories in the same transaction.
      insertCatStmt = db.compileStatement(INSERT_CATEGORY_STMT);
      List<String> categories = feed.getCategories();
      for (String cat : categories) {
        bindValues(feedId, cat, insertCatStmt);
        insertCatStmt.executeInsert();
      }

      if (isUpdated) {
        db.setTransactionSuccessful();
      }
    } finally {
      if (db.inTransaction()) {
        db.endTransaction();
      }
      IOUtils.closeStmt(insertItemStmt);
      IOUtils.closeStmt(insertCatStmt);
      db.close();
    }
    // always return true because there is no ability to rollback changes.
    return isUpdated;
  }

  private boolean isItemExists(long feedId, URL itemLink, SQLiteDatabase db) {
    Cursor c = db.query(Item.TABLE_NAME, ID_PROJECTION,
        buildStrictSelection(Item.LINK, Item.FEED_ID), new String[] {
            itemLink.toString(), String.valueOf(feedId)
        }, null, null, null);

    try {
      return c.moveToFirst();
    } finally {
      c.close();
    }
  }

  private NewsFeed findFeedCommon(String selection, String[] selectionArgs, boolean isLight) {
    SQLiteDatabase db = dbHelper.getReadableDatabase();
    Cursor feedCursor = null;
    try {
      feedCursor = db.query(Feed.TABLE_NAME, FULL_FEED_PROJECTION, selection, selectionArgs, null,
          null, Feed.DEFAULT_SORT_ORDER);

      if (feedCursor.moveToFirst()) {
        return cursorToFeed(feedCursor, isLight);
      }
    } finally {
      IOUtils.closeCursor(feedCursor);
      db.close();
    }
    return null;
  }

  private NewsFeed cursorToFeed(Cursor feedCursor, boolean isLight) {
    LightFeedBo feed = null;
    if (!feedCursor.isClosed()) {
      feed = isLight ? new LightFeedBo() : new FeedBo();
      long feedId = feedCursor.getLong(feedCursor.getColumnIndex(Feed._ID));
      feed.setId(feedId);
      feed.setTitle(feedCursor.getString(feedCursor.getColumnIndex(Feed.TITLE)));
      feed.setFeedUrl(feedCursor.getString(feedCursor.getColumnIndex(Feed.FEED_URL)));
      feed.setLink(feedCursor.getString(feedCursor.getColumnIndex(Feed.LINK)));
      List<String> categories = findCategories(feedId);
      feed.setCategories(categories);
      // do not load items if required light feed.
      if (!isLight) {
        List<ItemBo> feedItems = findFeedItems(feedId);
        feed.setItems(feedItems);
      }
    }
    return feed;
  }

  private ContentValues toContentValues(FeedBo feed) {
    ContentValues feedData = new ContentValues();
    feedData.put(Feed.TITLE, feed.getTitle());
    feedData.put(Feed.LINK, feed.getLink().toString());
    feedData.put(Feed.FEED_URL, feed.getFeedUrl().toString());
    URL iconUrl = feed.getIconUrl();
    if (iconUrl != null) {
      try {
        InputStream imageStream = iconUrl.openStream();
        byte[] iconContent = new byte[imageStream.available()];
        imageStream.read(iconContent);
        feedData.put(Feed.ICON, iconContent);
      } catch (IOException e) {
        Log.e(TAG, "Unable to obtain icon content.", e);
      }
    }
    return feedData;
  }

  private void bindValues(long feedId, ItemBo item, SQLiteStatement stmt) {
    stmt.clearBindings();
    stmt.bindLong(1, feedId);
    stmt.bindString(2, item.getTitle());
    stmt.bindString(3, item.getLink().toString());
    stmt.bindString(4, item.getDescription());
    stmt.bindLong(5, item.getPubDate().getTime());
  }

  private void bindValues(long feedId, String category, SQLiteStatement stmt) {
    stmt.clearBindings();
    stmt.bindLong(1, feedId);
    stmt.bindString(2, category);
  }

  private String buildStrictSelection(String... columns) {
    StringBuilder builder = new StringBuilder();
    boolean firstTime = true;
    for (String column : columns) {
      if (firstTime) {
        firstTime = false;
      } else {
        builder.append(" AND ");
      }
      builder.append(column).append(" = ?");
    }
    return builder.toString();
  }
}
